/*
 * File:    AbstractDiskBuffer.java
 * Created: 30-Oct-2007
 *
 * by Kenji Hollis <kenji@bitgatesoftware.com>
 * Released under the Creative Commons License version 2.5
 */

package com.bitgate.util.buffer;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This is an abstract implementation of a disk-based buffer.  These classes are automatically used in the {@link BufferFactory}
 * class, and should not be instantiated by the programmer directly.
 * 
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @since 0.1
 */
public class AbstractDiskBuffer implements BufferInterface {

	private final AtomicReference<RandomAccessFile> fileBuffer = new AtomicReference<RandomAccessFile>();
	private long limit;
	private String allocatedFilename;
	
	/**
	 * No-argument constructor.
	 * 
	 * @throws IOException if the disk buffer file could not be created.
	 */
	public AbstractDiskBuffer() throws IOException {
		limit = 0L;
		allocatedFilename = "disk-buffer-" + System.currentTimeMillis() + ".data";
		fileBuffer.set(new RandomAccessFile(allocatedFilename, "rwd"));
	}
	
	/**
	 * Creates a new disk buffer, copying the data in the constructor to the buffer for persistence to disk.  This is
	 * called by the {@link AbstractMemoryBuffer}, and is not available for standard use in the {@link BufferFactory}
	 * class.
	 * 
	 * @param data <code>byte[]</code> containing the data to copy.
	 * @throws IOException if the disk buffer file could not be created.
	 */
	public AbstractDiskBuffer(byte[] data) throws IOException {
		limit = 0L;
		allocatedFilename = "disk-buffer-" + System.currentTimeMillis() + ".data";
		fileBuffer.set(new RandomAccessFile(allocatedFilename, "rwd"));
	}
	
	public void appendData(byte[] data) throws BufferException {
		if (limit > 0) {
			if (data.length + size() > limit) {
				throw new BufferException("Buffer limit exceeded: limit is " + limit + " byte(s).");
			}
		}
		
		try {
			fileBuffer.get().seek(size());
			fileBuffer.get().write(data);
		} catch(IOException e) {
			throw new BufferException("Unable to append data.", e);
		}
	}

	public void clear() throws BufferException {
		try {
			fileBuffer.get().seek(0);
			fileBuffer.get().setLength(0);
		} catch(IOException e) {
			throw new BufferException("Unable to clear buffer.", e);
		}
	}

	public byte[] get() {
		long dataSize = 0;
		
		try {
			fileBuffer.get().seek(0);
			dataSize = size() - fileBuffer.get().getFilePointer();
		} catch(IOException e) {
			return null;
		}
		
		byte[] returnData = new byte[(int) dataSize];
		
		try {
			fileBuffer.get().read(returnData);
		} catch(IOException e) {
			return null;
		}
		
		return returnData;
	}
	
	public byte[] get(long size) {
		long dataSize = size;
		
		if (dataSize > size()) {
			dataSize = size();
		}
		
		byte[] returnData = new byte[(int) dataSize];
		
		try {
			fileBuffer.get().seek(0);
			fileBuffer.get().read(returnData);
		} catch(IOException e) {
			returnData = null;
		}
		
		return returnData;
	}
	
	public byte[] take() {
		long dataSize = 0;
		
		try {
			fileBuffer.get().seek(0);
			dataSize = size() - fileBuffer.get().getFilePointer();
		} catch(IOException e) {
			return null;
		}
		
		byte[] returnData = new byte[(int) dataSize];
		
		try {
			fileBuffer.get().read(returnData);
			fileBuffer.get().setLength(0L);
		} catch(IOException e) {
			return null;
		}
		
		return returnData;
	}

	public byte[] take(long size) {
		long dataSize = size;
		
		if (dataSize > size()) {
			dataSize = size();
		}
		
		byte[] returnData = new byte[(int) dataSize];
		
		try {
			fileBuffer.get().seek(0);
			fileBuffer.get().read(returnData);
		} catch(IOException e) {
			returnData = null;
		}
		
		return returnData;
	}

	public long getLimit() {
		return limit;
	}

	/**
	 * This member is available for you to use to insert data, however, it should probably not be used too often, as it
	 * requires the data be moved to the front of the buffer.  Instead of doing this, the class creates a new file buffer,
	 * writes the inserting data, and writes the remaining data from the old buffer to the new, which is a complete file
	 * copy operation.  This is extremely inefficient, and it should be avoided at all costs.  If you're grabbing the
	 * data from the file system in real time to send chunks of data, it's probably wiser to use a combination of this
	 * and a small memory buffer to &quot;packetize&quot; the data to be sent out.
	 * <p></p>
	 * That being said, I may write a class that does that automatically: one that packetizes the data in a 64K block
	 * or 32K block, whichever is more appropriate, and allows for segmented output of a buffer (or input.)
	 * <p></p>
	 * In the mean time, don't use this function.  It's not implemented yet.
	 * 
	 * @param data The <code>byte[]</code> data to insert.
	 * @throws BufferException on any errors.
	 */
	public void insertData(byte[] data) throws BufferException {
		throw new BufferException("This is not yet implemented.");
	}

	public BufferInterface persistToStorage() throws BufferException {
		throw new BufferException("Unable to persist disk data to further storage.");
	}

	public BufferInterface retrieveFromStorage() throws BufferException {
		BufferInterface bInterface = new AbstractMemoryBuffer(get());
		
		deallocate();
		
		return bInterface;
	}

	public void setLimit(long size) throws BufferException {
		long fileSize = size();
		
		if (size < fileSize) {
			throw new BufferException("Cannot set buffer size less than the current buffer size of " + fileSize + " byte(s).");
		}
		
		if (size < 0) {
			throw new IllegalArgumentException("Buffer size cannot be less than 0.");
		}
		
		limit = size;
	}

	public long size() {
		try {
			return fileBuffer.get().length();
		} catch(IOException e) {
			return 0L;
		}
	}
	
	public void deallocate() throws BufferException {
		try {
			fileBuffer.get().close();
		} catch(IOException e) {
			throw new BufferException("Unable to close buffer file.", e);
		} finally {
			(new File(allocatedFilename)).delete();
		}
	}
	
}